﻿''' <summary>
''' MPG PrettyList.  Note: This control automatically scales based on DPI.  The base designer DPI is ALWAYS 192 (VGA/WVGA).  Make sure your
''' iPrettListItems scale down to QVGA and square devices when rendering their content.  You can get the scaling factor by calling
''' the ReadOnly ActualScaleFactor property.
''' </summary>
Public Class PrettyList
  Private _originalItems As IEnumerable(Of iPrettyListItem)
  Private _items As New List(Of iPrettyListItem)
  Private _filterString As String = String.Empty
  Private _top As Integer = 0
  Private _listHeight As Integer = 0
  Private _listIsEmpty As String = ""

  ' Scrolling, graphics, and clicking variables
  Private _dpiFactor As Single = 0.0F
  Private _minPixelsToScroll As Integer = 20
  Private _okayToScroll As Boolean = False
  Private _okayToMouseClick As Boolean = False
  Private _mouseFirstPos As New Point(0, 0)
  Private _mouseLastPos As New Point(0, 0)
  Private _isMouseDown As Boolean = False
  Private _snapStopwatch As New System.Diagnostics.Stopwatch
  Private _snapPos As Integer = 0
  Private _snapUp As Boolean = False
  Private _suggestedColors As ListColors
  Private _scrollBarBackColor As Color
  Private _scrollBarForeColor As Color
  Private _scrollBarHLBackColor As Color
  Private _scrollBarHLForeColor As Color
  Private _brScFg As Brush = Nothing
  Private _brScBg As Brush = Nothing
  Private _brScHLFg As Brush = Nothing
  Private _brScHLBg As Brush = Nothing
  Private _brScFgDim As Brush = Nothing
  Private _brScBgDim As Brush = Nothing
  Private _pNavFG As Pen = Nothing
  Private _usefixedItemHeight As Boolean = False
  Private _fixedItemHeight = 1
  Private _overlayOffset As Integer = 0
  Private _isScrollBarScrolling As Boolean = False
  Private _lastDownPLI As iPrettyListItem = Nothing
  Private _navPLI As iPrettyListItem = Nothing
  Private _inContextMenu As Boolean = False

  ' Kinetic scrolling
  Private _kineticMouseLastYPos As Integer = 0
  Private _kineticStopWatch As New System.Diagnostics.Stopwatch
  Private _kineticDirectionIsDown As Boolean = False
  Private _kineticTimeQueue As New Queue(Of Long)
  Private _kineticRate As Integer = 0

  ' Events
  Public Event ItemClicked(ByVal sender As Object, ByVal clickedItem As iPrettyListItem, ByVal isLongClick As Boolean)
  Public Event ItemMouseDown(ByVal sender As Object, ByVal clickedItem As iPrettyListItem, ByRef canLongClick As Boolean)
  Public Event ItemMouseUp(ByVal sender As Object, ByVal clickedItem As iPrettyListItem)
  Public Event ItemMouseScroll(ByVal sender As Object, ByVal clickedItem As iPrettyListItem)
  Public Event NewItemsAssignedBegin(ByVal sender As Object)
  Public Event NewItemsAssignedEnd(ByVal sender As Object)
  Public Event FilterChanged(ByVal sender As Object, ByVal filter As String)

  ''' <summary>
  ''' Event that allows caller to draw an overlay over (or 'below' pushing upward; see remarks) the list.
  ''' </summary>
  ''' <param name="sender">List raising this event</param>
  ''' <param name="g">Graphics object to draw to</param>
  ''' <param name="width">Full width of the entire graphics objects</param>
  ''' <param name="height">Height of the list; Modify this value (passed byref) to indicate you want to "push" the list upwards, as if you are drawing below the list</param>
  ''' <remarks>If you modify the height value, which is passed by ref, you are indicating to the list that you want to "push" the list upwards, as if you are drawing below the list</remarks>
  Public Event DrawOverlay(ByVal sender As Object, ByVal g As Graphics, ByVal width As Integer, ByRef height As Integer)

  ' Structures
  Public Structure ListColors
    Dim BackgroundColor As Color
    Dim BackgroundColorAlternate As Color
    Dim BackgroundColorSelected As Color
    Dim TextColor1 As Color
    Dim TextColor2 As Color
    Dim TextColor3 As Color
    Dim TextColor4 As Color
    Dim TextcolorSelected As Color
    Dim NavigationFrameColor As Color
  End Structure

  Public Sub New()
    InitializeComponent()
    Me._dpiFactor = Me.AutoScaleDimensions.Height / 192.0F
    Me._suggestedColors.BackgroundColor = Me.BackColor
    Me._suggestedColors.BackgroundColorAlternate = Me.BackColor
    Me._suggestedColors.TextColor1 = Me.ForeColor
    Me._suggestedColors.TextColor2 = Me.ForeColor
    Me._suggestedColors.TextColor3 = Me.ForeColor
    Me._suggestedColors.NavigationFrameColor = Me.ForeColor
    Me._scrollBarBackColor = Me.BackColor
    Me._scrollBarForeColor = Me.ForeColor
  End Sub

  ''' <summary>
  ''' Fixed height means that whatever is the largest item in the list (in terms of height) will be the height of every item in the list.
  ''' Dynamic height lists mean that every item can have a different height.  Dynamic height lists require that every item be examined
  ''' on every redraw to determine the position of the items.  This has a linear degrading performance impact based on list size.  The
  ''' longer the list, the slower the redraw.
  ''' </summary>
  ''' <remarks>
  ''' Fixed height has a constant redraw speed because we can make assumptions about where
  ''' every item in the list is located.  By knowing where we are in the list (scroll position) we can immediately determine which item
  ''' is the top item on the screen.  Once we know that, which is one simple integer division, we simply start redrawing from there to the 
  ''' bottom of the screen.  In dynamic height lists, we have to ask for the height of every item in the list until we reach an item
  ''' that is on screen, then we draw those items.
  ''' </remarks>>
  Public Property UseFixedItemHeight() As Boolean
    Get
      Return Me._usefixedItemHeight
    End Get
    Set(ByVal value As Boolean)
      If Me._usefixedItemHeight <> value Then
        Me._usefixedItemHeight = value
        Me.RefreshItemRectangles()
      End If
    End Set
  End Property

  Public Property LongclickInterval() As Integer
    Get
      Return Me.TimerLongClick.Interval
    End Get
    Set(ByVal value As Integer)
      Me.TimerLongClick.Interval = value
    End Set
  End Property

  Public Property SuggestedItemColors() As ListColors
    Get
      Return Me._suggestedColors
    End Get
    Set(ByVal value As ListColors)
      Me._suggestedColors = value
    End Set
  End Property

  Private Function HalfColor(ByVal c As Color) As Color
    Return Color.FromArgb(c.R >> 1, c.G >> 1, c.B >> 1)
  End Function

  Public Property ScrollBarBackColor() As Color
    Get
      Return Me._scrollBarBackColor
    End Get
    Set(ByVal value As Color)
      Me._scrollBarBackColor = value
      Me._brScBg = Nothing
    End Set
  End Property

  Public Property ScrollBarForeColor() As Color
    Get
      Return Me._scrollBarForeColor
    End Get
    Set(ByVal value As Color)
      Me._scrollBarForeColor = value
      Me._brScFg = Nothing
    End Set
  End Property

  Public Property ScrollBarHighlightBackColor() As Color
    Get
      Return Me._scrollBarHLBackColor
    End Get
    Set(ByVal value As Color)
      Me._scrollBarHLBackColor = value
      Me._brScHLBg = Nothing
    End Set
  End Property

  Public Property ScrollBarHighlightForeColor() As Color
    Get
      Return Me._scrollBarHLForeColor
    End Get
    Set(ByVal value As Color)
      Me._scrollBarHLForeColor = value
      Me._brScHLFg = Nothing
    End Set
  End Property

  Public ReadOnly Property ActualScaleFactor()
    Get
      Return Me._dpiFactor
    End Get
  End Property

  Public Property ScrollPosition() As Integer
    Get
      Return -Me._top
    End Get
    Set(ByVal value As Integer)
      Me._top = -value
      Me.InvalidateSmart()
    End Set
  End Property

  Public Property ListIsEmptyString() As String
    Get
      Return Me._listIsEmpty
    End Get
    Set(ByVal value As String)
      Me._listIsEmpty = value
    End Set
  End Property

  Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
    If Me.Enabled AndAlso Not Me._inContextMenu Then
      Dim readyToLongClick As Boolean = False
      Dim allowedToLongClick As Boolean = False

      Me._isMouseDown = True

      ' Stop the snap or jump, if snapping or jumping!
      Me.TimerSnapScroll.Enabled = False
      Me.TimerJumpToItem.Enabled = False

      ' Assign the position information
      Me._mouseFirstPos = New Point(e.X, e.Y)
      Me._mouseLastPos = Me._mouseFirstPos

      ' Check the Y position to see if this is a "scrollbar scroll" (rapid scrolling on the right side)
      If e.X >= Me.Width - (Me._dpiFactor * 40) Then
        Me._isScrollBarScrolling = True
      End If

      ' Stop kinetic scroll or start long click, depending on the previous state
      If Me.TimerKineticScroll.Enabled Then
        Me.TimerKineticScroll.Enabled = False
        Me._okayToMouseClick = False
      ElseIf Not Me._isScrollBarScrolling Then
        Me._okayToMouseClick = True
        readytoLongClick = True
      End If

      ' Check to see if we are not scrolling (in some form) which means it's okay to do the mouse clicks
      If Not Me._isScrollBarScrolling AndAlso Me._okayToMouseClick Then ' If we are not scrolling...
        If Me._items IsNot Nothing Then
          If e.Y > 0 AndAlso e.Y < Me.Height - Me._overlayOffset Then
            For Each pli As iPrettyListItem In Me._items
              If pli.ClickRectangle.Contains(e.X, e.Y - Me._top) Then
                pli.OnMouseDown()
                RaiseEvent ItemMouseDown(Me, pli, allowedToLongClick)
                Me._lastDownPLI = pli
                Exit For
              End If
            Next
          End If
        End If
      End If

      ' set the long press timer
      If readyToLongClick AndAlso allowedToLongClick Then
        Me.TimerLongClick.Enabled = True
      End If

      ' Reset the kinetic scroll variables
      Me._kineticRate = 0
      Me._kineticTimeQueue.Clear()
      Me._kineticMouseLastYPos = Me._mouseLastPos.Y
      Me._kineticStopWatch.Reset()
      Me._kineticStopWatch.Start()
      Me._okayToScroll = False
    End If

    Me._inContextMenu = False

    MyBase.OnMouseDown(e)
    Me.InvalidateSmart()
  End Sub

  Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
    If Me.Enabled AndAlso Me._isMouseDown AndAlso Not Me._inContextMenu Then
      Dim newPos = New Point(e.X, e.Y)
      Dim newTime As Date = Date.Now

      If Not Me._okayToScroll _
       AndAlso Math.Abs(e.Y - Me._mouseFirstPos.Y) > (Me._minPixelsToScroll * Me._dpiFactor) _
      Then
        Me.TimerLongClick.Enabled = False
        Me._okayToScroll = True
        If Me._lastDownPLI IsNot Nothing Then
          Me._lastDownPLI.OnMouseScroll()
          RaiseEvent ItemMouseScroll(Me, Me._lastDownPLI)
        End If
      End If

      If Me._okayToScroll Then
        If Me._isScrollBarScrolling AndAlso Me._listHeight > Me.Height Then
          Dim scrollThrow As Integer = Me._listHeight - (Me.Height - Me._overlayOffset) ' How long is the "throw" (top to bottom scrollable range for me._top)
          Dim topBottomOffset = Me._dpiFactor * 80
          Dim h As Integer = Me.Height - (Me._overlayOffset + topBottomOffset)

          Me._top = -(scrollThrow) * ((e.Y - (topBottomOffset >> 1)) / h)

          If Me._top < -scrollThrow Then
            Me._top = -scrollThrow
          ElseIf Me._top > 0 Then
            Me._top = 0
          End If
        Else
          Me._top += e.Y - Me._mouseLastPos.Y
        End If

        Me.InvalidateSmart()
      End If

      If Not Me._isScrollBarScrolling AndAlso Me._okayToScroll Then
        If Me._kineticStopWatch.ElapsedMilliseconds > 250 Then
          Me._kineticStopWatch.Stop()
          Me._kineticStopWatch.Reset()
          Me._kineticTimeQueue.Clear()
          Me._kineticMouseLastYPos = e.Y
          Me._kineticStopWatch.Start()
        ElseIf Math.Abs(Me._kineticMouseLastYPos - e.Y) > (Me._minPixelsToScroll * Me._dpiFactor) Then
          Dim curDirIsDown As Boolean = (e.Y > Me._kineticMouseLastYPos)

          If curDirIsDown <> Me._kineticDirectionIsDown AndAlso Me._kineticTimeQueue.Count > 0 Then
            Me._kineticTimeQueue.Clear()
          ElseIf Me._kineticTimeQueue.Count >= 3 Then
            Me._kineticTimeQueue.Dequeue()
          End If

          Me._kineticStopWatch.Stop()
          Me._kineticTimeQueue.Enqueue(Me._kineticStopWatch.ElapsedMilliseconds)
          Me._kineticStopWatch.Reset()
          Me._kineticDirectionIsDown = curDirIsDown
          Me._kineticMouseLastYPos = e.Y
          Me._kineticStopWatch.Start()
        End If
      End If

      Me._mouseLastPos = newPos
    End If

    MyBase.OnMouseMove(e)
  End Sub

  Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)
    Me.TimerLongClick.Enabled = False

    If Me.Enabled AndAlso Not Me._inContextMenu AndAlso Me._isMouseDown Then
      If Me._lastDownPLI IsNot Nothing Then
        Me._lastDownPLI.OnMouseUp()
        RaiseEvent ItemMouseUp(Me, Me._lastDownPLI)
      End If

      If Me._okayToScroll Then
        If Me._isScrollBarScrolling AndAlso Me._listHeight > Me.Height Then
          Dim scrollThrow As Integer = Me._listHeight - (Me.Height - Me._overlayOffset) ' How long is the "throw" (top to bottom scrollable range for me._top)
          Dim topBottomOffset = Me._dpiFactor * 80
          Dim h As Integer = Me.Height - (Me._overlayOffset + topBottomOffset)

          Me._top = -(scrollThrow) * ((e.Y - (topBottomOffset >> 1)) / h)

          If Me._top < -scrollThrow Then
            Me._top = -scrollThrow
          ElseIf Me._top > 0 Then
            Me._top = 0
          End If
        Else
          Me._top += e.Y - Me._mouseLastPos.Y
        End If
      ElseIf Me._lastDownPLI IsNot Nothing AndAlso Me._okayToMouseClick Then
        Me._lastDownPLI.OnMouseClick()
        RaiseEvent ItemClicked(Me, Me._lastDownPLI, False)
      End If

      Me._mouseLastPos = New Point(e.X, e.Y)
      Me._isMouseDown = False
      Me._isScrollBarScrolling = False

      If Not Me.DoSnap() AndAlso Me._kineticTimeQueue.Count > 0 Then
        Me._kineticStopWatch.Stop()
        If Me._kineticStopWatch.ElapsedMilliseconds < 250 Then
          Dim distance As Integer = Me._minPixelsToScroll * Me._dpiFactor
          Dim vels As New List(Of Single) ' in pix/100ms
          Dim pix As Single = 0.0F

          Me._kineticRate = 0
          For Each v As Long In Me._kineticTimeQueue
            If v > 0 Then
              pix = CSng(distance / v) * Me.TimerKineticScroll.Interval
            Else
              pix = Me.Height / 2 ' Max speed
            End If
            If pix > Me.Height / 2 Then
              pix = Me.Height / 2 ' Max speed
            End If

            Me._kineticRate += CInt(pix)
          Next

          Me._kineticRate /= Me._kineticTimeQueue.Count

          If Not Me._kineticDirectionIsDown Then
            Me._kineticRate = -Me._kineticRate
          End If

          Me.TimerKineticScroll.Enabled = True
        End If
        Me._kineticStopWatch.Reset()
      End If
    End If

    Me._lastDownPLI = Nothing

    MyBase.OnMouseUp(e)
    Me.InvalidateSmart()
  End Sub

  Public Function DoSnap() As Boolean
    If Not Me._isMouseDown AndAlso Me._listHeight > 0 AndAlso Not Me.TimerSnapScroll.Enabled AndAlso Not Me.TimerKineticScroll.Enabled Then
      Dim ht As Integer = 0
      If Me._listHeight < Me.Height - Me._overlayOffset Then
        ht = Me._listHeight
      Else
        ht = Me.Height - Me._overlayOffset
      End If
      If Me._top > 0 OrElse (Me._listHeight > 0 AndAlso ht - Me._top > Me._listHeight) Then
        If Me._top > 0 Then
          Me._snapUp = True
        Else
          Me._snapUp = False
        End If
        Me._snapStopwatch.Reset()
        Me._snapPos = Me._top
        Me._snapStopwatch.Start()
        Me.TimerSnapScroll.Enabled = True
        Return True
      End If
    End If
    Return False
  End Function

  Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
    Static Dim bufWidth As Integer = 0
    Static Dim bufHeight As Integer = 0
    Static Dim buffer As Bitmap = Nothing
    Static Dim overlayBuffer As Bitmap = Nothing
    Static Dim brBg As New SolidBrush(Me.BackColor)
    Static Dim brFg As New SolidBrush(Me.ForeColor)
    Static Dim scrWidth As Integer = 6 * Me._dpiFactor

    If Me._brScBg Is Nothing Then
      Me._brScBg = New SolidBrush(Me._scrollBarBackColor)
      Me._brScBgDim = New SolidBrush(Me.HalfColor(Me._scrollBarBackColor))
    End If

    If Me._brScFg Is Nothing Then
      Me._brScFg = New SolidBrush(Me._scrollBarForeColor)
      Me._brScFgDim = New SolidBrush(Me.HalfColor(Me._scrollBarForeColor))
    End If

    If Me._brScHLBg Is Nothing Then
      Me._brScHLBg = New SolidBrush(Me._scrollBarHLBackColor)
    End If

    If Me._brScHLFg Is Nothing Then
      Me._brScHLFg = New SolidBrush(Me._scrollBarHLForeColor)
    End If

    If Me._pNavFG Is Nothing Then
      Me._pNavFG = New Pen(Me._suggestedColors.NavigationFrameColor, 2 * Me._dpiFactor)
    End If

    If Me.BackColor <> brBg.Color Then
      brBg = New SolidBrush(Me.BackColor)
    End If

    If Me.ForeColor <> brFg.Color Then
      brFg = New SolidBrush(Me.ForeColor)
    End If

    If buffer Is Nothing OrElse Me.Width <> bufWidth OrElse Me.Height <> bufHeight Then
      bufWidth = Me.Width
      bufHeight = Me.Height

      If buffer IsNot Nothing Then buffer.Dispose()
      If overlayBuffer IsNot Nothing Then overlayBuffer.Dispose()

      buffer = New Bitmap(bufWidth, bufHeight)
      overlayBuffer = New Bitmap(bufWidth, bufHeight)
    End If

    Dim g As Graphics = Graphics.FromImage(buffer)
    g.FillRectangle(brBg, 0, 0, bufWidth, bufHeight)

    Dim og As Graphics = Graphics.FromImage(overlayBuffer)
    og.FillRectangle(brBg, 0, 0, bufWidth, bufHeight)

    ' Allow the owner to draw an overlay
    Dim newHeight As Integer = bufHeight
    RaiseEvent DrawOverlay(Me, og, bufWidth, newHeight)
    Me._overlayOffset = bufHeight - newHeight

    ' Do we need scroll bars? - Initial values, calculated later...
    Dim needScrollBars As Boolean = False
    Dim itemRightOffset As Integer = 0

    ' Draw all items that are currently visible
    If Me._items IsNot Nothing AndAlso Me._items.Count > 0 Then
      Dim curtop As Integer = Me._top
      Dim isAlternate As Boolean = False
      Dim rect As Rectangle

      ' Calculate the list height
      If Me._usefixedItemHeight Then
        Me._listHeight = Me._items.Count * Me._fixedItemHeight
      Else
        Me._listHeight = 0
        Dim en As IEnumerator(Of iPrettyListItem) = Me._items.GetEnumerator
        While en.MoveNext
          Me._listHeight += en.Current.DrawPrettyListItemHeight(g, Me._dpiFactor)
        End While
      End If

      ' Do we need scroll bars? - Calculated here
      needScrollBars = Me._listHeight > (Me.Height - Me._overlayOffset)
      itemRightOffset = CInt(IIf(needScrollBars, scrWidth, 0))

      ' Draw the list
      If Me._usefixedItemHeight Then
        Dim start As Integer = (-Me._top / Me._fixedItemHeight) - 1 ' We start one less than the calculation so that we draw the bottom of the item that is falling off the screen at the top.

        If start > Me._items.Count - 1 Then
          start = Me._items.Count - 1
        End If

        If start < 0 Then
          start = 0
        End If

        If start Mod 2 = 0 Then
          isAlternate = True
        End If

        curtop += (start * Me._fixedItemHeight)

        For i = start To Me._items.Count - 1
          rect.Y = curtop
          rect.X = 0
          rect.Height = Me._fixedItemHeight
          rect.Width = Me.Width - itemRightOffset

          If curtop < Me.Height Then
            Me._items(i).DrawPrettyListItem(g, Me._dpiFactor, Me._suggestedColors, rect, isAlternate)
          Else
            Exit For
          End If

          curtop += rect.Height
          isAlternate = Not isAlternate
        Next
      Else
        Dim en As IEnumerator(Of iPrettyListItem) = Me._items.GetEnumerator

        While en.MoveNext
          rect.Y = curtop
          rect.X = 0
          rect.Height = en.Current.DrawPrettyListItemHeight(g, Me._dpiFactor)
          rect.Width = Me.Width - itemRightOffset

          If rect.Bottom > 0 AndAlso curtop < Me.Height Then
            en.Current.DrawPrettyListItem(g, Me._dpiFactor, Me._suggestedColors, rect, isAlternate)
          End If

          curtop += rect.Height
          isAlternate = Not isAlternate
        End While
      End If
    End If

    ' Dim image if control is disabled
    If Not Me.Enabled Then
      Me.DimBitmapByHalf(buffer)
    End If

    ' Draw the key nav overlay
    If Me._navPLI IsNot Nothing Then
      Static Dim oPad As Integer = Me._pNavFG.Width - 1
      Static Dim oPadDepth As Integer = (Me._pNavFG.Width << 1) - 1
      g.DrawRectangle(Me._pNavFG, Me._navPLI.ClickRectangle.X + oPad, Me._navPLI.ClickRectangle.Y + Me._top + oPad, Me._navPLI.ClickRectangle.Width - (oPadDepth + itemRightOffset), Me._navPLI.ClickRectangle.Height - oPadDepth)
    End If

    ' Copy the overlay to the main bitmap
    g.DrawImage(overlayBuffer, 0, newHeight, New Rectangle(0, newHeight, bufWidth, bufHeight - newHeight), GraphicsUnit.Pixel)

    ' If the list is empty...
    If Me._items Is Nothing OrElse Me._items.Count <= 0 Then
      With g.MeasureString(Me._listIsEmpty, Me.Font)
        g.DrawString(Me._listIsEmpty, Me.Font, brFg, (Me.Width - .Width) >> 1, ((Me.Height - Me._overlayOffset) - .Height) >> 1)
      End With
    Else
      ' Draw scrollbar overlay
      If needScrollBars Then
        Dim scrLeft As Integer = Me.Width - scrWidth
        Dim scrHeight As Integer = newHeight * (newHeight / Me._listHeight)

        If scrHeight < scrWidth Then
          scrHeight = scrWidth
        End If

        Dim scrTop As Integer = (newHeight - scrHeight) * ((0 - Me._top) / (Me._listHeight - newHeight))

        If scrTop + scrHeight > newHeight Then
          scrHeight = newHeight - scrTop
          If scrHeight < 0 Then
            scrHeight = 0
          End If
        End If

        Dim bgb As Brush = Me._brScBg
        Dim fgb As Brush = Me._brScFg

        If Me.Enabled Then
          If Me._isScrollBarScrolling Then
            bgb = Me._brScHLBg
            fgb = Me._brScHLFg
          Else
            bgb = Me._brScBg
            fgb = Me._brScFg
          End If
        Else
          bgb = Me._brScBgDim
          fgb = Me._brScFgDim
        End If

        g.FillRectangle(bgb, New Rectangle(scrLeft, 0, scrWidth, newHeight))
        g.FillRectangle(fgb, New Rectangle(scrLeft, scrTop, scrWidth, scrHeight))
      End If
    End If

    ' Clean up the graphics objects
    g.Dispose()
    og.Dispose()

    ' Check if we need to snap (if height of list changed or something)
    If Not Me._isMouseDown Then Me.DoSnap()

    ' Copy back buffer to the screen
    e.Graphics.DrawImage(buffer, 0, 0)
  End Sub

  Protected Overrides Sub OnPaintBackground(ByVal e As System.Windows.Forms.PaintEventArgs)
    'MyBase.OnPaintBackground(e)
  End Sub

  Public Property Items() As IEnumerable(Of iPrettyListItem)
    Get
      Return Me._originalItems
    End Get
    Set(ByVal value As IEnumerable(Of iPrettyListItem))
      Me._kineticRate = 0
      Me._kineticTimeQueue.Clear()
      Me._kineticStopWatch.Reset()
      Me._okayToScroll = False
      RaiseEvent NewItemsAssignedBegin(Me)
      Me._originalItems = value
      Me._filterString = String.Empty
      Me.SetFilteredItems()
      RaiseEvent NewItemsAssignedEnd(Me)
    End Set
  End Property

  Public ReadOnly Property FilteredItems() As List(Of iPrettyListItem)
    Get
      Return Me._items
    End Get
  End Property

  Public ReadOnly Property IsItemInFilteredList(ByVal ipl As iPrettyListItem)
    Get
      Return Me._items.Contains(ipl)
    End Get
  End Property

  Private Sub SetFilteredItems()
    If Me._originalItems IsNot Nothing Then
      Me._items.Clear()
      If Me._filterString.Length = 0 Then
        For Each it As iPrettyListItem In Me._originalItems
          Me._items.Add(it)
        Next
      Else
        Dim fstrup As String = Me._filterString.ToUpper
        For Each it As iPrettyListItem In Me._originalItems
          If it.FilterIsIncluded(fstrup) Then
            Me._items.Add(it)
          End If
        Next
      End If
    End If

    Me._navPLI = Nothing
    Me._lastDownPLI = Nothing
    Me._isMouseDown = False
    Me._okayToMouseClick = False
    Me._okayToScroll = False
    Me._top = 0
    Me._kineticRate = 0

    Me.RefreshItemRectangles()
    Me.InvalidateSmart()
  End Sub

  Public Property Filter() As String
    Get
      Return Me._filterString
    End Get
    Set(ByVal value As String)
      Me._filterString = value
      Me.SetFilteredItems()
      RaiseEvent FilterChanged(Me, Me._filterString)
    End Set
  End Property

  Public Sub AddFilterKey(ByVal key As Char)
    If key = ControlChars.Back Then
      If Me._filterString.Length > 0 Then
        Me._filterString = Me._filterString.Substring(0, Me._filterString.Length - 1)
      End If
    ElseIf key > ChrW(13) Then ' Avoid all the control characters
      Me._filterString &= key
    End If

    Me.SetFilteredItems()
    RaiseEvent FilterChanged(Me, Me._filterString)
  End Sub

  ''' <summary>
  ''' This method recalculates the rectangles used to represent every item in the list.  The rectangles are used for determining if the
  ''' user has clicked on a particular item.  This is called each time a new enumerable list is assigned to the PrettyList or when fixed
  ''' height mode is changed.  Generally you never need to call this method.  However, if in your item class anything can change its
  ''' height by changing a value, this method needs to be called after the height changes.  PrettyList has no way of knowing (without
  ''' performance degradation) when item height changes.  The easiest way to deal with this is to create an event that is raised by your
  ''' items when their height changes.  When your class listening for the event receives the event, it can call this method.
  ''' </summary>
  Public Sub RefreshItemRectangles()
    If Me._items IsNot Nothing Then
      Dim en As IEnumerator(Of iPrettyListItem) = Me._items.GetEnumerator

      If Me._usefixedItemHeight Then
        Dim maxHeight As Integer = 1
        Dim y As Integer = 0

        Using img As New Drawing.Bitmap(Me.Width, Me.Height)
          Using g As Graphics = Drawing.Graphics.FromImage(img)
            While en.MoveNext
              Dim h As Integer = en.Current.DrawPrettyListItemHeight(g, Me._dpiFactor)
              If h > maxHeight Then
                maxHeight = h
              End If
            End While
          End Using
        End Using

        Me._fixedItemHeight = maxHeight

        en.Reset()
        While en.MoveNext
          en.Current.ClickRectangle = New Rectangle(0, y, Me.Width, maxHeight)
          y += maxHeight
        End While
      Else
        Using img As New Drawing.Bitmap(Me.Width, Me.Height)
          Using g As Graphics = Drawing.Graphics.FromImage(img)

            Dim y As Integer = 0

            While en.MoveNext
              Dim h As Integer = en.Current.DrawPrettyListItemHeight(g, Me._dpiFactor)
              en.Current.ClickRectangle = New Rectangle(0, y, Me.Width, h)
              y += h
            End While
          End Using
        End Using
      End If
    End If
  End Sub

  Private Sub TimerSnapScroll_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerSnapScroll.Tick
    Static inFunc As Boolean = False

    Me.TimerJumpToItem.Enabled = False

    If Not inFunc Then
      inFunc = True
      Dim distance As Single = 0.0F

      distance = (200.0F - CSng(Me._snapStopwatch.ElapsedMilliseconds)) / 200.0F

      If distance < 0 Then
        distance = 0
      End If

      If Me._snapUp Then
        Me._top = Me._snapPos * distance

        If Me._top <= 0 Then
          Me._top = 0
          Me._snapStopwatch.Stop()
          Me.TimerSnapScroll.Enabled = False
        End If
      Else
        If Me._listHeight > (Me.Height - Me._overlayOffset) Then
          Me._top = ((Me.Height - Me._overlayOffset) - Me._listHeight) + ((Me._snapPos - ((Me.Height - Me._overlayOffset) - Me._listHeight)) * distance)
          If (Me._top + Me._listHeight) >= (Me.Height - Me._overlayOffset) Then
            Me._top = (Me.Height - Me._overlayOffset) - Me._listHeight
            Me._snapStopwatch.Stop()
            Me.TimerSnapScroll.Enabled = False
          End If
        Else
          Me._top = Me._snapPos * distance
          If Me._top >= 0 Then
            Me._top = 0
            Me._snapStopwatch.Stop()
            Me.TimerSnapScroll.Enabled = False
          End If
        End If
      End If

      Me.InvalidateSmart()
      inFunc = False
    End If
  End Sub

  Private Sub TimerLongClick_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerLongClick.Tick
    Me.TimerLongClick.Enabled = False

    If Me.Enabled AndAlso Me._items IsNot Nothing Then
      For Each pli As iPrettyListItem In Me._items
        If pli.ClickRectangle.Contains(Me._mouseFirstPos.X, Me._mouseFirstPos.Y - Me._top) Then
          Me._isMouseDown = False
          Me._okayToScroll = False
          Me._inContextMenu = True
          pli.OnMouseLongClick()
          RaiseEvent ItemClicked(Me, pli, True)
          Exit For
        End If
      Next
    End If

    Me._inContextMenu = False
  End Sub

  Private Sub TimerKineticScroll_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerKineticScroll.Tick
    Static repeat As Integer = 0

    If Me.Enabled AndAlso Math.Abs(Me._kineticRate) > 0 Then
      Me._top += Me._kineticRate

      If Me._top > 0 OrElse (Me.Height - Me._overlayOffset) - Me._top > Me._listHeight Then
        If Math.Abs(Me._kineticRate) >= (20 * Me._dpiFactor) Then
          Me._kineticRate /= 2
        ElseIf Math.Abs(Me._kineticRate) > 1 Then
          Me._kineticRate *= 0.1
        Else
          Me._kineticRate = 0
        End If
      ElseIf repeat > 0 Then
        repeat -= 1
      Else
        repeat = Math.Abs((Me._kineticRate / Me._dpiFactor) / 3)
        Me._kineticRate /= 2
      End If

      Me.InvalidateSmart()
      Else
        Me.TimerKineticScroll.Enabled = False
        repeat = 0

        If Me._top > 0 OrElse (Me.Height - Me._overlayOffset) - Me._top > Me._listHeight Then
          If Me._top > 0 Then
            Me._snapUp = True
          Else
            Me._snapUp = False
          End If
          Me._snapStopwatch.Reset()
          Me._snapPos = Me._top
          Me._snapStopwatch.Start()
          Me.TimerSnapScroll.Enabled = True
        End If
      End If
  End Sub


  Private Sub DoJump()
    Me.TimerJumpToItem.Enabled = True
    Me.TimerJumpToItem_Tick(Me, EventArgs.Empty)
  End Sub

  Private Sub TimerJumpToItem_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerJumpToItem.Tick
    If Me._navPLI Is Nothing Then
      Me.TimerJumpToItem.Enabled = False
    Else
      Dim nTop As Integer = Me._navPLI.ClickRectangle.Top + Me._top
      Dim nBottom As Integer = Me._navPLI.ClickRectangle.Bottom + Me._top
      Dim trueHeight As Integer = Me.Height - Me._overlayOffset

      If nTop < 0 Then
        Dim move As Integer = -nTop >> 1
        If move < (Me._dpiFactor * 10) Then
          Me._top -= nTop ' Remember, nTop is a negative number, need to subtract to keep the sign correct
        Else
          Me._top += move
        End If
        Me.InvalidateSmart()
      ElseIf nBottom > trueHeight Then
        Dim move As Integer = (nBottom - trueHeight) >> 1
        If move < (Me._dpiFactor * 10) Then
          Me._top -= nBottom - trueHeight
        Else
          Me._top -= move
        End If
        Me.InvalidateSmart()
      Else
        Me.TimerJumpToItem.Enabled = False
      End If

      ' Make sure we haven't exceeded bounds by doing a snap!
      If Me.DoSnap() Then
        Me.TimerJumpToItem.Enabled = False ' disable if we are snapping
      End If
    End If
  End Sub


  Public Sub ClearSelectedItems()
    For Each item As iPrettyListItem In Me._items
      item.IsSelected = False
    Next
    Me.InvalidateSmart()
  End Sub

  Public Sub NavPreviousPLI()
    If Me._items IsNot Nothing Then
      If Me._navPLI Is Nothing Then
        Me._navPLI = Me._items.Last
      Else
        Me._navPLI.IsSelected = False
        For i As Integer = 0 To Me._items.Count - 1
          If Me._items(i) Is Me._navPLI Then
            If i = 0 Then
              Me._navPLI = Me._items.Last
            Else
              Me._navPLI = Me._items(i - 1)
            End If
            Exit For
          End If
        Next
      End If
    End If
    Me.DoJump()
    Me.InvalidateSmart()
  End Sub

  Public Sub NavNextPLI()
    If Me._items IsNot Nothing Then
      If Me._navPLI Is Nothing Then
        Me._navPLI = Me._items.First
      Else
        Me._navPLI.IsSelected = False
        For i As Integer = 0 To Me._items.Count - 1
          If Me._items(i) Is Me._navPLI Then
            If i < Me._items.Count - 1 Then
              Me._navPLI = Me._items(i + 1)
            Else
              Me._navPLI = Me._items.First
            End If
            Exit For
          End If
        Next
      End If
    End If
    Me.DoJump()
    Me.InvalidateSmart()
  End Sub

  Public Sub NavSelectDownPLI()
    Dim allowedToLongClick As Boolean = False
    If Me._items IsNot Nothing AndAlso Me._navPLI IsNot Nothing Then
      Me._navPLI.IsSelected = True
      Me._navPLI.OnMouseDown()

      RaiseEvent ItemMouseDown(Me, Me._navPLI, allowedToLongClick)

      If allowedToLongClick Then
        Me.TimerLongClick.Enabled = True
      End If

      Me._lastDownPLI = Me._navPLI
      Me._okayToMouseClick = True
      Me.InvalidateSmart()
    End If
  End Sub

  Public Sub NavSelectUpPLI()
    Me.TimerLongClick.Enabled = False
    If Me._items IsNot Nothing AndAlso Me._lastDownPLI IsNot Nothing Then
      Me._lastDownPLI.IsSelected = False
      Me._lastDownPLI.OnMouseUp()
      RaiseEvent ItemMouseUp(Me, Me._lastDownPLI)

      If Me._okayToMouseClick Then
        Me._lastDownPLI.OnMouseClick()
        RaiseEvent ItemClicked(Me, Me._lastDownPLI, False)
      End If

      Me.InvalidateSmart()
    End If
  End Sub

  Public Sub NavContextPLI()
    If Me._items IsNot Nothing AndAlso Me._lastDownPLI IsNot Nothing Then
      Me._lastDownPLI.IsSelected = False
      Me._lastDownPLI.OnMouseLongClick()
      RaiseEvent ItemClicked(Me, Me._lastDownPLI, True)
      Me.InvalidateSmart()
    End If
  End Sub







  Public Sub InvalidateSmart()
    Me.TimerInvalidate.Enabled = True
  End Sub

  Private Sub TimerInvalidate_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerInvalidate.Tick
    Me.TimerInvalidate.Enabled = False
    Me.Invalidate()
  End Sub

  Protected Overrides Sub OnEnabledChanged(ByVal e As System.EventArgs)
    Me.Invalidate() ' Can't do a "smart" invalidate since all timers get disabled when the control does
    MyBase.OnEnabledChanged(e)
  End Sub

  Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
    Me.Invalidate() ' Can't do a "smart" invalidate since all timers get disabled when the control does
    MyBase.OnResize(e)
  End Sub




  ''' <summary>
  ''' Takes a source bitmap and dims all the pixels by half (bit-shift each pixel to the right)
  ''' </summary>
  ''' <param name="bmp">A bitmap object that contains the image to be modified.</param>
  Private Sub DimBitmapByHalf(ByRef bmp As Bitmap)
    Dim bmd As Imaging.BitmapData = bmp.LockBits(New Rectangle(0, 0, bmp.Width, bmp.Height), Imaging.ImageLockMode.ReadWrite, Imaging.PixelFormat.Format16bppRgb565)
    Try
      Dim bufLen As Integer = (bmd.Stride * bmd.Height) / 2
      Dim buf(bufLen - 1) As Short
      Dim r As Short
      Dim g As Short
      Dim b As Short

      System.Runtime.InteropServices.Marshal.Copy(bmd.Scan0, buf, 0, bufLen)
      For i = 0 To bufLen - 1
        '                              RRRRR GGGGGG BBBBB
        r = (buf(i) >> 1) And &H7800 ' 01111 000000 00000
        g = (buf(i) >> 1) And &H3E0  ' 00000 011111 00000
        b = (buf(i) >> 1) And &HF    ' 00000 000000 01111
        buf(i) = r Or g Or b
      Next
      System.Runtime.InteropServices.Marshal.Copy(buf, 0, bmd.Scan0, bufLen)
    Finally
      bmp.UnlockBits(bmd)
    End Try
  End Sub

End Class
